例外処理 指針 2024版
#例外処理
難しく考えることはない。「契約」を捉え直せば、シンプルになる。
「エラーをreturnで返す」「いやいや、throwで例外を..!!!」みたいな議論あるけど、あれは手段の話。後にしよう。
まずは以下2つの本質を理解しておくと良い。手段はその都度最適なものを選べ。
たった50行なので、忘れた際には隅々まで読んで思い出せ!!!
1. 「契約」をもう少し広く考えると、エラー(失敗)と例外の違いも見えてくる
契約プログラミングで出てくる「契約」という考え方。
これは、「メニュー表」と捉えればOK。
「この値なら、こういう挙動をします」という取り決めを、外に宣言してるにすぎない。
ただ、この「契約」に対する考えを俺は狭くしてた。
正常系の処理でしか契約を宣言してなくて、正しい引数を与えられたのにエラーになるのは「契約違反!」として突っぱねてたらから、実装にうまく取り入れれてなかった。
code: sample
// 今までは以下のような感じで考えた。ので、実装でうまく表現できなかった。使いにくかった。
Hogeクラス.hogeメソッドのメニュー表
1. 引数aが0~10なら、正常系の結果を渡します。
※ 引数の範囲が守られてても、何か内部でうまくいかんかったら例外!クラッシュ!!
2. 引数aが-1以下、11以上なら、契約違反で例外!!クラッシュ!!
// もう少し「契約」を広げてみる
1. 引数aが0~10なら、正常系の結果渡します
2. 引数aが0~10でも、DBの~~エラーの関係で失敗する可能性あります
3. 引数aが0~10でも、...の関係で失敗する可能性あります
4. 引数aが0~10以外なら、呼び出し元の違反として失敗させてエラー返します
※ 上記以外の失敗が起きたら、予期してないエラーだ!つまり、例外!!クラッシュ!!!
下の方は、「失敗」する可能性もメニューに入れてる。
こうすることで、呼び出し元はより使いやすくなる。
もし呼び出し元で回復できる失敗があるなら、そこで調整できるようになる。
対処できない失敗は、上に回せばいいだけやし。(例外中立性)
「失敗」したから即クラッシュさせるんじゃなくて、予期できるエラーだからこそメニュー表に含めれる。
逆にいうと、メニュー表にある失敗以外こそが「予期せぬ失敗(=例外)」ってこと
予期せぬ失敗は、即クラッシュでいい。なぜなら、呼び出し元がその失敗に対する準備ができてないから。
予期できてないんやから、無理に回復処理なんてしてはいけない。即失敗させよう。
そして、なぜ失敗が起きたのかを調査し、予期した失敗に変換する、もしくは、コードを修正していく。
全ての失敗要因を予期できてたら、こんなことせんでいいけど、そんなん無理やから、こういうやり方を取るのが最適だと思う。
でも流石に、失敗が大きなコストに繋がる場所では、事前にもっと固くテストとかしないとダメ。
2. 処理が失敗した場合は、例外安全性を意識して...
例外安全性って考え方がある。
要するに、その処理が失敗した際には、その処理で関わったものを綺麗に直しておけ!みたいな話。
安全性にはレベルが3つある。
1. 基本的安全
処理が失敗しても、不変条件やリソース状態が安全に保たれてるようにする。(回復をちゃんとする)
2. 強い安全
トランザクションのロールバックをイメージすればOK。
失敗した際、処理の途中で変更してしまった状態などあれば、元に戻しておく。
3. no fail
正直、よくわからんonigiri.w2.icon
まあ、いずれにせよ、処理が失敗した際には、その処理の責任として、その処理に関わる場所の安全性は担保するって話。
安全性の担保は、レベルが高いほど良いが、まあ無理して2以上にする必要もない。1が最低限守れてればOK。
1を守りつつ、より上位のレベルは、上のルーチンに任せるってのもありだと思う(例外中立性)
例えば、トランザクションのロールバックとか。トランザクションを貼ってるのは、上位ルーチンだったりするので、そこまで伝播させて、データベースという状態の「強い安全」を守ってもらおう。
そのルーチンが失敗したということは、鎖で繋がってる上位ルーチンそれぞれも失敗したということ。
それぞれのルーチンで、失敗したことによる自分が関わる箇所の安全性を考えることが必須なのです。
これ、ミュータブルなものを操作してたりすると安全性保つのが面倒だが、なんとイミュータブルを使ってると勝手に守られます。
だから、積極的に不変というものを意識しようってなる
表明(assersion)はいつ使う?
コンパイルで安全を保証できないが、開発者がロジックを見る限りはそのエラーが起きえない場合。
つまり、万一起こるはずないが、それが起きたらエラーになるって時に念のためのガードとして使う。
例えば以下のような例が上がる
1. アルゴリズムが正確であることをチェック
code: sample.ts
const sorted = functionSort(...);
assert(isSort(sorted));
2. 不変条件の保証
code: sample.ts
...
assert(this.name > 0);
3. undefinedであることは確実にない
code: sample.ts
assert(hoge !== undefined);
...
などなど
プログラマから見たら、絶対にあり得ない、起き得ないことなのだが、
今後の変更とか考えた際に起き得るかも知らんとか、アルゴリズムがミスって変更された時に起きるかも知らん
などなど。そういう万一が起きた時にすぐチェックできるようにしておく。